package com.miniotest.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import com.miniotest.constants.FileConstants;
import com.miniotest.pojo.dto.FileDTO;
import com.miniotest.common.Result;
import com.miniotest.service.FileService;
import com.miniotest.utils.FFMpegUtils;
import com.miniotest.utils.MinioUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;

@Slf4j
@Service
public class FileServiceImpl implements FileService {
    @Resource
    private MinioUtils minioUtils;
    @Resource
    private FFMpegUtils ffMpegUtils;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource(name = "minIOUploadTreadPool")
    private ThreadPoolTaskExecutor poolTaskExecutor;

    @Value("${spring.file.temppath}")
    private String tempFileFolder;


    /**
     * 文件上传初始化 ——判断文件是否存在(实现秒传) ——初始化reids
     * @param fileDTO
     * @return
     */
    public Result<String> uploadInit(FileDTO fileDTO){
        String md5 = fileDTO.getMd5();
        Integer segNum = fileDTO.getSegNum();

        // 判断文件是否存在 todo 引入数据库查

        //初始化redis——将md5作为key，以set形式存入所有分片序号
        for(int i=0;i<segNum;i++){
            redisTemplate.opsForSet().add(md5,i);
        }
        return Result.success(FileConstants.FILE_INIT_SUCCEED);
    }

    /**
     * 文件上传
     * @param fileDTO
     * @return
     */
    public Result<String> upload(FileDTO fileDTO){
        MultipartFile resourceFile = fileDTO.getFile(); // 文件实体
        String md5 = fileDTO.getMd5(); // 文件md5
        Integer segNum = fileDTO.getSegNum(); // 总分片数
        Integer segIndex = fileDTO.getSegIndex(); // 当前文件分片号
        String fileName = md5.concat("_").concat(segIndex.toString()); // 分片文件文件名: md5_segIndex
        // 判空
        if(null == fileDTO.getFile()){
            return Result.error(FileConstants.SEG_FILE_NOT_EXISTS);
        }
        // 将文件上传至本地临时路径
        // (1)构造本地临时目录存放分片文件—— tempFileFolder \\ md5 \\ tmp
        String fileFolderPath = tempFileFolder.concat("\\").concat(md5).concat("\\").concat("tmp"); // 文件上级目录
        // (2)将文件写入—— tempFileFolder \\ md5 \\ tmp \\ fileName
        File fileFolder = new File(fileFolderPath);
        if(!fileFolder.exists()){
            fileFolder.mkdirs();
        }
        File targetFile = new File(fileFolder,fileName);
        try {
            FileUtils.copyInputStreamToFile(resourceFile.getInputStream(),targetFile);
            // (3)移除redis分片记录
            redisTemplate.opsForSet().remove(md5,segIndex);
        } catch (IOException e) {
            log.error("分片文件存入本地失败");
            return Result.error(FileConstants.SEG_FILE_UPLOAD_FAILED);
        }
        return Result.success(FileConstants.SEG_FILE_UPLOAD_SUCCEED);
    }
    /**
     * 文件上传结束 ——断点续传判断——本地分片文件合并——转m3u8——上传至Minio
     * @param fileDTO
     * @return
     */
    public Result<Map<String,Set>> uploadDone(FileDTO fileDTO) {
        String md5 = fileDTO.getMd5();
        //本地临时目录存放分片文件—— tempFileFolder \\ md5 \\ tmp
        String fileFolderPath = tempFileFolder.concat("\\").concat(md5).concat("\\").concat("tmp");
        // 结果封装
        Map<String,Set> resultMap = new HashMap<>();
        // 断点续传判断
        if(redisTemplate.hasKey(md5)){
            Set<Integer> indexSet = redisTemplate.opsForSet().members(md5);
            resultMap.put(FileConstants.SEG_FILE_LOST,indexSet);
            log.info("断点续传:{}",indexSet);
            return Result.error(resultMap);
        }
        // 文件合并
        String mergeFilePath = mergeFile(md5,fileFolderPath);
        if(null == mergeFilePath){
            resultMap.put(FileConstants.FILE_MERGE_FAILED,null);
            return Result.error(resultMap);
        }
        // 文件转码m3u8 ——tempFileFolder \\ md5 \\ m3u8 \\ fileName.m3u8
        String m3u8FilePath = ffMpegUtils.localFileToM3u8(mergeFilePath,md5);
        if(null == m3u8FilePath){
            resultMap.put(FileConstants.FILE_TRANSFERS_TO_CODE_FAILED,null);
            return Result.error(resultMap);
        }
        /*// m3u8文件上传
        try {
            List<File> errorFileList = uploadM3u8ToMinio(m3u8FilePath);
            // todo 对上传至minio失败的流媒体文件作重传
            if(!CollectionUtil.isEmpty(errorFileList)){
                log.error("存在未上传的本地流媒体文件");
            }

        } catch (InterruptedException e) {
            log.error("流媒体文件上传失败",e);
        }*/

        resultMap.put(FileConstants.FILE_UPLOAD_SUCCEED,null);
        return Result.success(resultMap);
    }

    /**
     * 合并本地分片文件
     * @param fileNamePrefix 本地分片文件名前缀——本地分片文件存储为: md5_segIndex,则fileNamePrefix为md5
     * @param parentFileFolderPath 本地分片文件所在目录 tempFileFolder \\ md5 \\ tmp
     * @return
     */
    public String mergeFile(String fileNamePrefix,String parentFileFolderPath){
        // 合并后文件的文件名
        String fileName = fileNamePrefix;
        // 合并文件所在路径 tempFileFolder \\ md5 \\ fileName
        String targetFilePath = parentFileFolderPath.substring(0,parentFileFolderPath.lastIndexOf("\\")+1).concat(fileName);
        // 临时文件所在目录 tempFileFolder \\ md5 \\ tmp
        File parentFileFolder = new File(parentFileFolderPath);
        // 合并后的文件
        File targetFile = new File(targetFilePath);
        try {
            // 遍历所有分片文件进行合并
            for(int i=0;i< Objects.requireNonNull(parentFileFolder.listFiles()).length;i++){
                File segFile = new File(parentFileFolder,fileNamePrefix+"_"+i);
                // 获取目标文件输出流，设置为非覆盖
                FileOutputStream targetFileOps = new FileOutputStream(targetFile,true);
                FileUtils.copyFile(segFile,targetFileOps);
                targetFileOps.close();
            }
            // 删除临时文件所在目录
            FileUtils.deleteDirectory(parentFileFolder);
            return targetFilePath;
        } catch (Exception e) {
            log.error("本地分片文件合并失败",e);
            return null;
        }
    }

    /**
     * 将转码后的m3u8文件上传至minio
     * @param m3u8FilePath ——tempFileFolder \\ md5 \\ m3u8 \\ fileName.m3u8
     * @return
     */
    public List<File> uploadM3u8ToMinio(String m3u8FilePath) throws InterruptedException {
        log.info("将流媒体文件上传至minio");
        // m3u8文件所在目录 tempFileFolder \\ md5 \\ m3u8
        String resourceFileFolderPath = m3u8FilePath.substring(0,m3u8FilePath.lastIndexOf("\\"));
        File resourceFileFolder = new File(resourceFileFolderPath);

        // 获取目录下所有文件 .m3u8 + .ts
        File[] allFiles = resourceFileFolder.listFiles();
        // 记录上传失败的文件
        List<File> errorFileList = new ArrayList<>();

        // 开计时锁多线程传输
        CountDownLatch countDownLatch = new CountDownLatch(allFiles.length);
        Arrays.stream(allFiles).forEach(i->poolTaskExecutor.execute(()->{
           try(FileInputStream ips = new FileInputStream(i)){
               String fileName = i.getName();
                minioUtils.uploadByIps(ips,fileName);
           }catch (Exception e){
               log.error("部分流媒体文件上传至minio失败",e);
               errorFileList.add(i);
           }finally {
               countDownLatch.countDown();
           }
        }));
        countDownLatch.await();
        log.info("流媒体文件上传至minio成功");

        // 返回未上传至minio的流媒体文件列表
        if(!CollectionUtil.isEmpty(errorFileList)){
            return errorFileList;
        }

        // 开启线程删除本地保存的流媒体文件以及临时目录
        poolTaskExecutor.execute(()->{
            try {
                // 要删除的目录——tempFileFolder \\ md5
                String deleteFolderPath = resourceFileFolderPath.substring(0,resourceFileFolderPath.lastIndexOf("\\"));
                File deleteFolder = new File(deleteFolderPath);
                FileUtils.deleteDirectory(deleteFolder);
            } catch (IOException e) {
                log.error("删除本地流媒体文件失败",e);
            }
        });
        return null;
    }
}
